本文将分析 QEMU TCG 模式下的访存模型,也就是 softmmu 的设计,基于的版本为 QEMU 6.2,架构则以 RISC-V 为例。

基本调用链

1. target/riscv/translate.c 访存指令翻译。
2. accel/tcg/cputlb.c 调用 helper 加载函数(如 helper_le_ldq_mmu)。
3. 调用 load_helper 函数
	1. 查 TLB,若未命中,则 tlb_fill 进行填充。
	2. 处理各种特殊情况(MMIO、不对界访问等)。
	3. 计算得到对应的宿主机虚拟地址 haddr = addr + entry->addend,并根据字长进行访问。

TLB 数据结构

QEMU 的 softmmu 模型的核心数据结构为其 TLB 的设计,结构如下:

CPUTLB

typedef struct CPUTLB {
    CPUTLBCommon c;  // 存储 TLB 的一系列元数据。
    CPUTLBDesc d[NB_MMU_MODES];  // 慢速(二级) TLB,主要用于存储从一级 TLB 中被驱逐(evict)出的条目。
    CPUTLBDescFast f[NB_MMU_MODES];  // 快速(一级) TLB,用于快速完成地址转换。
} CPUTLB;

CPUTLBDesc

typedef struct CPUTLBDesc {
    /* 大页处理相关 */
    target_ulong large_page_addr;
    target_ulong large_page_mask;
    
    /* 与 TLB 动态调整相关 */
    int64_t window_begin_ns;
    size_t window_max_entries;
    size_t n_used_entries;
    
   	/* vTLB 中表中使用的下一个索引 */
    size_t vindex;

    /* 二级 TLB(vTLB) 和二级 IOTLB(vIOTLB) */
    CPUTLBEntry vtable[CPU_VTLB_SIZE];
    CPUIOTLBEntry viotlb[CPU_VTLB_SIZE];
    /* 一级 IOTLB,与 IOMMU 相关(不太了解) */
    CPUIOTLBEntry *iotlb;
} CPUTLBDesc;

CPUTLBDescFast

typedef struct CPUTLBDescFast {
    uintptr_t mask;  // 用于完成 (address, mmu_idx) -> TLB_index 的映射
    CPUTLBEntry *table;  // 一级 TLB 表
} CPUTLBDescFast QEMU_ALIGNED(2 * sizeof(void *));

CPUTLBEntry

typedef struct CPUTLBEntry {
    union {
        struct {
            /* 用于与 address 对比判断是否命中 */
            target_ulong addr_read;
            target_ulong addr_write;
            target_ulong addr_code;
            /* 宿主机虚拟地址 haddr 与模拟器虚拟地址 address 的偏移量,用于地址转换 */
            uintptr_t addend;
        };
        uint8_t dummy[1 << CPU_TLB_ENTRY_BITS];
    };
} CPUTLBEntry;

这里为读、写、执行都分别设置一个地址字段,其实是一种空间换时间的策略。比如说一个页面(如地址为 addr)具有 可读可写但不可执行 的权限,那么在进行 TLB 填充时,字段 addr_readaddr_write 都会被赋上 addr 的值,而 addr_code 则为(无符号)-1。这样在后续进行 TLB 命中判定时,本次是什么访问方式就与哪个字段进行比对,那么自然,如果本次针对 addr 的访问是取址访问(执行),自然就会发生 TLB miss。

这样的设计可以使得 TLB 命中判定仅由一条 cmp 指令来完成,而如果使用类似页表条目的设计方法,引入一些权限位来标识页面是否可读可写可执行,空间占用自然更少,但同时比对效率也更低。

内存访问

load_helper

load_helper/store_helper 是 QEMU softmmu 访存的核心函数,作用是根据 addr 和访问类型来对指定的模拟器内存进行对应的读/写操作。本文只分析 load_helperstore_helper 的实现与其类似。

/*
 * env: CPU 架构相关的状态寄存器集合
 * addr: 要读取的模拟器目标虚拟地址
 * oi: 内存操作索引(包含 Memop 和 mmu_idx)
 * retaddr: 调用者返回地址(用于异常处理)
 * op: 内存操作类型(大小、端序)
 * code_read: 标志(是取指还是数据访问)
 * full_load: 用于递归处理不对界/跨页
 */
static inline uint64_t QEMU_ALWAYS_INLINE
load_helper(CPUArchState *env, target_ulong addr, MemOpIdx oi,
            uintptr_t retaddr, MemOp op, bool code_read,
            FullLoadHelper *full_load)
{
    uintptr_t mmu_idx = get_mmuidx(oi);
    uintptr_t index = tlb_index(env, mmu_idx, addr);
    CPUTLBEntry *entry = tlb_entry(env, mmu_idx, addr);
    target_ulong tlb_addr = code_read ? entry->addr_code : entry->addr_read;
    const size_t tlb_off = code_read ?
        offsetof(CPUTLBEntry, addr_code) : offsetof(CPUTLBEntry, addr_read);
    const MMUAccessType access_type =
        code_read ? MMU_INST_FETCH : MMU_DATA_LOAD;
    unsigned a_bits = get_alignment_bits(get_memop(oi));
    void *haddr;
    uint64_t res;
    size_t size = memop_size(op);

    /* 处理架构相关的访存不对界异常 */
    if (addr & ((1 << a_bits) - 1)) {
        cpu_unaligned_access(env_cpu(env), addr, access_type,
                             mmu_idx, retaddr);
    }

    /* 判断一级 TLB 是否命中  */
    if (!tlb_hit(tlb_addr, addr)) {
        /* 
         * 判断二级 TLB 是否命中
         * 若二级 TLB 命中,则将二级 TLB 中的条目与 addr 对应的一级 TLB
         * 中的条目进行交换,此后 entry 将为正确的条目。
         */
        if (!victim_tlb_hit(env, mmu_idx, index, tlb_off,
                            addr & TARGET_PAGE_MASK)) {
            /* 若二级 TLB 未命中,则需要进行填充。 */
            tlb_fill(env_cpu(env), addr, size,
                     access_type, mmu_idx, retaddr);
            index = tlb_index(env, mmu_idx, addr);
            entry = tlb_entry(env, mmu_idx, addr);
        }
        tlb_addr = code_read ? entry->addr_code : entry->addr_read;
        tlb_addr &= ~TLB_INVALID_MASK;
    }

    /* 处理一些特殊情况(TLB 的 tlb_addr 的低位存储着一些属性位) */
    if (unlikely(tlb_addr & ~TARGET_PAGE_MASK)) {
        CPUIOTLBEntry *iotlbentry;
        bool need_swap;

        if ((addr & (size - 1)) != 0) {
            goto do_unaligned_access;
        }

        iotlbentry = &env_tlb(env)->d[mmu_idx].iotlb[index];

        /* 处理观测点访问  */
        if (unlikely(tlb_addr & TLB_WATCHPOINT)) {
            cpu_check_watchpoint(env_cpu(env), addr, size,
                                 iotlbentry->attrs, BP_MEM_READ, retaddr);
        }

        /* 判断是否需要端序交换 */
        need_swap = size > 1 && (tlb_addr & TLB_BSWAP);

        /* 处理 I/O 访问 */
        if (likely(tlb_addr & TLB_MMIO)) {
            return io_readx(env, iotlbentry, mmu_idx, addr, retaddr,
                            access_type, op ^ (need_swap * MO_BSWAP));
        }

        haddr = (void *)((uintptr_t)addr + entry->addend);

        /* 两个 load_memop 分开写便于编译器优化(不太懂) */
        if (unlikely(need_swap)) {
            return load_memop(haddr, op ^ MO_BSWAP);
        }
        return load_memop(haddr, op);
    }

    /* 处理慢速的不对界访问 (横跨多个页面或者 I/O).  */
    if (size > 1
        && unlikely((addr & ~TARGET_PAGE_MASK) + size - 1
                    >= TARGET_PAGE_SIZE)) {
        target_ulong addr1, addr2;
        uint64_t r1, r2;
        unsigned shift;
    do_unaligned_access:
        addr1 = addr & ~((target_ulong)size - 1);
        addr2 = addr1 + size;
        r1 = full_load(env, addr1, oi, retaddr);
        r2 = full_load(env, addr2, oi, retaddr);
        shift = (addr & (size - 1)) * 8;

        if (memop_big_endian(op)) {
            res = (r1 << shift) | (r2 >> ((size * 8) - shift));
        } else {
            res = (r1 >> shift) | (r2 << ((size * 8) - shift));
        }
        return res & MAKE_64BIT_MASK(0, size * 8);
    }

    /* 加上 TLB 条目的 addend 偏移量得到宿主机的虚拟地址 */
    haddr = (void *)((uintptr_t)addr + entry->addend);
    return load_memop(haddr, op);
}

TLB 填充

tlb_set_page

/* 
 * cpu: CPU 数据结构
 * vaddr: 虚拟地址
 * paddr: 虚拟地址对应的物理地址
 * attrs: 内存事务属性,通常为 UNSPECIFIED
 * prot: 访问权限(读/写/执行)
 * mmu_idx: 地址空间标识符
 * size: 映射大小(支持大页)
 */
void tlb_set_page_with_attrs(CPUState *cpu, target_ulong vaddr,
                             hwaddr paddr, MemTxAttrs attrs, int prot,
                             int mmu_idx, target_ulong size)
{
    CPUArchState *env = cpu->env_ptr;
    CPUTLB *tlb = env_tlb(env);
    CPUTLBDesc *desc = &tlb->d[mmu_idx];
    MemoryRegionSection *section;
    unsigned int index;
    target_ulong address;
    target_ulong write_address;
    uintptr_t addend;
    CPUTLBEntry *te, tn;
    hwaddr iotlb, xlat, sz, paddr_page;
    target_ulong vaddr_page;
    int asidx = cpu_asidx_from_attrs(cpu, attrs);
    int wp_flags;
    bool is_ram, is_romd;

    assert_cpu_is_self(cpu);

    if (size <= TARGET_PAGE_SIZE) {
        sz = TARGET_PAGE_SIZE;
    } else {
        /* 记录大页信息 */
        tlb_add_large_page(env, mmu_idx, vaddr, size);
        sz = size;
    }
    vaddr_page = vaddr & TARGET_PAGE_MASK;
    paddr_page = paddr & TARGET_PAGE_MASK;

    /*
     * 将物理内存区域转换为对应的内存区域 MemoryRegionSection
     * 并获取内存区域的偏移量 xlat,实际可用大小 sz 和访问权限 prot
     */
    section = address_space_translate_for_iotlb(cpu, asidx, paddr_page,
                                                &xlat, &sz, attrs, &prot);
    assert(sz >= TARGET_PAGE_SIZE);

    tlb_debug("vaddr=" TARGET_FMT_lx " paddr=0x" TARGET_FMT_plx
              " prot=%x idx=%d\n",
              vaddr, paddr, prot, mmu_idx);

    address = vaddr_page;
    /* 映射小于页大小(奇怪的情况?) */
    if (size < TARGET_PAGE_SIZE) {
        /* 使得 TLB 条目无效化  */
        address |= TLB_INVALID_MASK;
    }
    if (attrs.byte_swap) {
        address |= TLB_BSWAP;
    }

    is_ram = memory_region_is_ram(section->mr);
    is_romd = memory_region_is_romd(section->mr);

    if (is_ram || is_romd) {
        addend = (uintptr_t)memory_region_get_ram_ptr(section->mr) + xlat;
    } else {
        addend = 0;
    }

    write_address = address;
    if (is_ram) {
        iotlb = memory_region_get_ram_addr(section->mr) + xlat;
        if (prot & PAGE_WRITE) {
            if (section->readonly) {
                write_address |= TLB_DISCARD_WRITE;
            } else if (cpu_physical_memory_is_clean(iotlb)) {
                write_address |= TLB_NOTDIRTY;
            }
        }
    } else {
        iotlb = memory_region_section_get_iotlb(cpu, section) + xlat;
        write_address |= TLB_MMIO;
        if (!is_romd) {
            address = write_address;
        }
    }

    /* 检测当前页面是否设置了监视点 */
    wp_flags = cpu_watchpoint_address_matches(cpu, vaddr_page,
                                              TARGET_PAGE_SIZE);

    index = tlb_index(env, mmu_idx, vaddr_page);
    te = tlb_entry(env, mmu_idx, vaddr_page);

    qemu_spin_lock(&tlb->c.lock);

    /* 标记 TLB 为脏  */
    tlb->c.dirty |= 1 << mmu_idx;

    /* 确保 vTLB 中没有 vaddr 的缓存 */
    tlb_flush_vtlb_page_locked(env, mmu_idx, vaddr_page);

    /* 
     * 如果对应 TLB 条目位置现已存在其他 vaddr 的条目,
     * 则将其驱逐至 vTLB 中
     */
    if (!tlb_hit_page_anyprot(te, vaddr_page) && !tlb_entry_is_empty(te)) {
        unsigned vidx = desc->vindex++ % CPU_VTLB_SIZE;
        CPUTLBEntry *tv = &desc->vtable[vidx];

        copy_tlb_helper_locked(tv, te);
        desc->viotlb[vidx] = desc->iotlb[index];
        tlb_n_used_entries_dec(env, mmu_idx);
    }

    desc->iotlb[index].addr = iotlb - vaddr_page;
    desc->iotlb[index].attrs = attrs;

    /* 设置 addend 字段,使得 vaddr_page + addend = haddr */
    tn.addend = addend - vaddr_page;
    /* 设置可读的条目 */
    if (prot & PAGE_READ) {
        tn.addr_read = address;
        if (wp_flags & BP_MEM_READ) {
            tn.addr_read |= TLB_WATCHPOINT;
        }
    } else {
        tn.addr_read = -1;
    }

    /* 设置可执行的条目 */
    if (prot & PAGE_EXEC) {
        tn.addr_code = address;
    } else {
        tn.addr_code = -1;
    }

    /* 设置可写的条目 */
    tn.addr_write = -1;
    if (prot & PAGE_WRITE) {
        tn.addr_write = write_address;
        if (prot & PAGE_WRITE_INV) {
            tn.addr_write |= TLB_INVALID_MASK;
        }
        if (wp_flags & BP_MEM_WRITE) {
            tn.addr_write |= TLB_WATCHPOINT;
        }
    }

    /* 更新 TLB 条目 */
    copy_tlb_helper_locked(te, &tn);
    tlb_n_used_entries_inc(env, mmu_idx);
    qemu_spin_unlock(&tlb->c.lock);
}

void tlb_set_page(CPUState *cpu, target_ulong vaddr,
                  hwaddr paddr, int prot,
                  int mmu_idx, target_ulong size)
{
    tlb_set_page_with_attrs(cpu, vaddr, paddr, MEMTXATTRS_UNSPECIFIED,
                            prot, mmu_idx, size);
}

大页处理

一个值得一提的内容是 QEMU TLB 对大页的处理,可能也是为了性能的权衡,QEMU 对此的策略就是不支持。

当向 TLB 中填充页大小大于 TARGET_PAGE_SIZE 的条目时,QEMU 会调用 tlb_add_large_page 进行大页的记录,代码如下:

static void tlb_add_large_page(CPUArchState *env, int mmu_idx,
                               target_ulong vaddr, target_ulong size)
{
    target_ulong lp_addr = env_tlb(env)->d[mmu_idx].large_page_addr;
    target_ulong lp_mask = ~(size - 1);

    if (lp_addr == (target_ulong)-1) {
        /* 此前未记录大页  */
        lp_addr = vaddr;
    } else {
        /* 扩展已存在的大页来将新的区域包含进去 */
        lp_mask &= env_tlb(env)->d[mmu_idx].large_page_mask;
        while (((lp_addr ^ vaddr) & lp_mask) != 0) {
            lp_mask <<= 1;  // 扩大掩码直到覆盖新地址
        }
    }
    env_tlb(env)->d[mmu_idx].large_page_addr = lp_addr & lp_mask;
    env_tlb(env)->d[mmu_idx].large_page_mask = lp_mask;
}

它的基本逻辑就是将本次访存的地址和大小记录下来,如果先前已经记录过大页,那么则将其记录的掩码进行扩大,以覆盖本次记录的大页的范围。

具体来说,对于一个 2MB 大页,它在进行 TLB 填充时,每次只会填一个 4KB 小页。但是在 Guest 系统层,它认为存在这么一个 2MB 的大页,因此在它想要无效化大页条目时,我们需要将单独进行填充的若干个小页条目全部无效化,为此 QEMU 采取了一种保守做法:直接将该 mmu_idx 下的所有的 TLB 条目全部刷新。代码如下:

static void tlb_flush_page_locked(CPUArchState *env, int midx,
                                  target_ulong page)
{
    target_ulong lp_addr = env_tlb(env)->d[midx].large_page_addr;
    target_ulong lp_mask = env_tlb(env)->d[midx].large_page_mask;

    /* Check if we need to flush due to large pages.  */
    if ((page & lp_mask) == lp_addr) {
        tlb_debug("forcing full flush midx %d ("
                  TARGET_FMT_lx "/" TARGET_FMT_lx ")\n",
                  midx, lp_addr, lp_mask);
        tlb_flush_one_mmuidx_locked(env, midx, get_clock_realtime());
    } else {
        if (tlb_flush_entry_locked(tlb_entry(env, midx, page), page)) {
            tlb_n_used_entries_dec(env, midx);
        }
        tlb_flush_vtlb_page_locked(env, midx, page);
    }
}

参考资料